package nf.graph;

import java.util.List;
import java.util.Set;
import java.util.HashSet;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.HashMap;
import java.util.zip.Adler32;
import nf.util.MappedCollection;

/**
 * A node in a graph.
 */
public class Node implements PropertyBag, Comparable<Node>
{
	private Group group;
	private Map<String, Object> properties;
	private String name;
	
	public Node()
	{
		this(null, null);
	}
	
	public Node(String name)
	{
		this(name, null);
	}
	
	Node(String name, Group group)
	{
		setName(name);
		setGroup(group);
		properties = new HashMap<String, Object>();
	}
	
	public Node copy()
	{
		Node n = createNewInstance();
		n.properties = new HashMap<String, Object>(properties);
		n.setName(name);
		
		return n;
	}
	
	protected Node createNewInstance()
	{
		return new Node();
	}
	
	/**
	 * Returns the node's name
	 * @return Node's name
	 */
	public String getName()
	{
	    return name;
	}
	
	public void setName(String name)
	{
		this.name = name;
	}
	
	/**
	 * Returns the graph to which this node belongs
	 * @return Graph to which this node belongs
	 */
	public Graph getGraph()
	{
	    return (group != null)
			? group.getGraph()
			: null;
	}
	
	public int getDegree()
	{
		try {
			return getNeighbors().size();
		} catch (NullPointerException e) {
			return 0;
		}
	}
	
	public List<Node> getNeighbors()
	{
		Graph graph = getGraph();
		return (graph != null)
			? graph.getNeighbors(this)
			: null;
	}
	
	public List<Node> getPredecessors()
	{
		Graph graph = getGraph();
		return (graph != null)
			? graph.getPredecessors(this)
			: null;
	}
	
	public Collection<Edge> getEdges()
	{
		return new EdgeCollection();
	}
	
	public boolean isNeighbor(Node other)
	{
		Graph graph = getGraph();
		return (graph != null && graph == other.getGraph())
			? graph.connected(this, other)
			: false;
	}
	
	public Path shortestPathTo(Node other)
	{
		Graph graph = getGraph();
		return (graph != null && graph == other.getGraph())
			? graph.findShortestPath(this, other)
			: null;
	}
	
	public Object getProperty(String name)
	{
		return properties.get(name);
	}
	
	public Object setProperty(String name, Object newValue)
	{
		return properties.put(name, newValue);
	}
	
	public boolean isReachable(Node other)
	{
		return (null != shortestPathTo(other));
	}
	
	public Group enclosingGroup(Node other)
	{
		Graph myGraph = getGraph();
		Graph otherGraph = other.getGraph();
		Group otherGroup = other.getGroup();
		
		if (myGraph == null || myGraph != otherGraph)
			return null;
		
		if (group == otherGroup)
			return group;
		
		return myGraph.commonGroup(this, other);
	}
	
	public String toString()
	{
		String propString = (!properties.isEmpty())
			? " " + properties.toString()
			: "";
		return (name == null)
			? String.format("<%s @ 0x%X%s>", getClass().getName(), hashCode(),
				propString)
			: String.format("<%s %s%s>", getClass().getName(), name,
				propString);
	}

	public int hashCode()
	{
		Adler32 hasher;

		if (name == null)
			return super.hashCode();
		
		hasher = new Adler32();
		hasher.update(name.getBytes());
		return (int) hasher.getValue();
	}
	
	public boolean equals(Object object)
	{
		try {
			Node other = (Node) object;
			if (other == null || name == null)
				return super.equals(object);
			return name.equals(other.getName());
		} catch (ClassCastException e) {
			return false;
		}
	}
	
	private int getNumericName()
	{
		if (name == null) {
			throw new NumberFormatException("name is null");
		}
		
		return Integer.parseInt(name);
	}
	
	public int compareTo(Node other)
	{
		String otherName = other.getName();
		
		try {
			int numName = getNumericName();
			int otherNumName = other.getNumericName();
			
			return numName - otherNumName;
		} catch (NumberFormatException e) {
			// treat both names as as text
		}
		
		if (this.name == null) {
			return (otherName == null)
				? 0
				: 1;
		} else {
			return (otherName == null)
				? -1
				: name.compareTo(otherName);
		}
	}
	
	/**
	 * Returns the group to which this node belongs
	 * @return Group to which this node belongs, or null if it belongs to none
	 */
	public Group getGroup()
	{
	    return group;
	}
	
	void setGroup(Group group)
	{
		this.group = group;
	}
	
	private class EdgeCollection extends MappedCollection<Node, Edge>
	{
		public EdgeCollection()
		{
			super(getNeighbors());
		}
		
		protected Edge translate(Node neighbor)
		{
			return new Edge(Node.this, neighbor);
		}
	}
}
